1st TQLCTF Review

Misc

wordle

附件中给出了如下源代码以及需要用到的单词。

import os
import random
from flag import award

random.seed(os.urandom(64))

with open('allowed_guesses.txt', 'r') as f:
    allowed_guesses = set([x.strip() for x in f.readlines()])

with open('valid_words.txt', 'r') as f:
    valid_words = [x.strip() for x in f.readlines()]


MAX_LEVEL = 512
GREEN = '\033[42m  \033[0m'
YELLOW = '\033[43m  \033[0m'
WHITE = '\033[47m  \033[0m'

def get_challenge():
    # id = random.getrandbits(32)
    # answer = valid_words[id % len(valid_words)]
    # return hex(id)[2:].zfill(8), answer

    # To prevent the disclosure of answer
    id = random.randrange(len(valid_words) * (2 ** 20))
    answer = valid_words[id % len(valid_words)]
    id = (id // len(valid_words)) ^ (id % len(valid_words))
    return hex(id)[2:].zfill(5), answer

def check(answer, guess):
    answer_chars = []
    for i in range(5):
        if guess[i] != answer[i]:
            answer_chars.append(answer[i])
    result = []
    for i in range(5):
        if guess[i] == answer[i]:
            result.append(GREEN)
        elif guess[i] in answer_chars:
            result.append(YELLOW)
            answer_chars.remove(guess[i])
        else:
            result.append(WHITE)
    return ' '.join(result)

def game(limit):
    round = 0
    while round < MAX_LEVEL:
        round += 1
        id, answer = get_challenge()
        print(f'Round {round}: #{id}')
        correct = False
        for _ in range(limit):
            while True:
                guess = input('> ')
                if len(guess) == 5 and guess in allowed_guesses:
                    break
                print('Invalid guess')
            result = check(answer, guess)
            if result == ' '.join([GREEN] * 5):
                print(f'Correct! {result}')
                correct = True
                break
            else:
                print(f'Wrong!   {result}')
        if not correct:
            print('You failed...')
            return round - 1

    return MAX_LEVEL


def choose_mode():
    print('Choose gamemode:')
    print('0: Easy mode')
    print('1: Normal mode')
    print('2: Hard mode')
    print('3: Insane mode')
    # print('4: Expert mode')
    # print('-1: Osu! mode')
    mode = int(input('> '))
    assert 0 <= mode <= 3
    return mode

if __name__ == '__main__':
    print('Guess the WORDLE in a few tries.')
    print('Each guess must be a valid 5 letter word.')
    print('After each guess, the color of the tiles will change to show how close your guess was to the word.')

    while True:
        mode = choose_mode()
        if mode == 0:
            limit = 999999999
        else:
            limit = 7 - mode
        final_level = game(limit)
        if final_level < MAX_LEVEL:
            pass
        else:
            print('You are the Master of WORDLE!')
        flag = award(mode, final_level)
        print(f'Here is you award: {flag}')

其中关键部分是 while Truerandom.seed(os.urandom(64))。在一轮游戏结束之后由于循环使得随机数种子不会变换。而使用模式 0 可以爆破猜解单词从而算出生成的随机数,因此可以尝试随机数预测。

环境搭建

附件中的源码将获取 flag 的逻辑去掉之后可以成功运行。因此用 ncat 监听一个端口即可模拟远程。在 Linux 下使用如下指令即可。

 ncat -lvp 1234 -e "/usr/bin/python3  main.py"

Windows 下的 Netcat 由于换行的差异会导致脚本与实际远程不一致。

随机数预测

模式 0 下的游戏拥有很多次猜解机会,因此可以尝试使用正则的办法逐步排除,从而得出正确的单词。进一步可以使用如下代码计算出生成的随机数 id。

hexId = int(hexId, 16)
moded = WORDS.index(word)
id = (hexId ^ moded) * len(WORDS) + moded

其中的 hexId 是题目给出的 round 的 id。

使用 randcrack 库可以完成随机数预测的工作,只需要收集 624 个正确的结果即可预测后面 624 个随机数。因此写出如下代码来梭这道题。

# wordleLib
import pwn
import re
import string

FILE = open("G:\\valid_words.txt", "rb").read().decode()
WORDS = FILE.split("\n")
GREEN = '\033[42m  \033[0m'
YELLOW = '\033[43m  \033[0m'
WHITE = '\033[47m  \033[0m'


def guess(proc: pwn.remote):
    print("=== Guess Start ===")

    # Receive the hexId
    # print(f"Receive generated id {proc.recvline().decode().strip()}")
    line = proc.recvline_regex(r"Round (\d{1,3}): #([\w\d]+)\n")
    round, hexId = re.findall(r"Round (\d{1,3}): #([\w\d]+)", line.decode())[0]
    round = int(round)

    print(f"Round {round}, hexId {hexId}")

    wrongs = []
    alphabet = string.ascii_lowercase
    MATCH = [f"[{alphabet}]", f"[{alphabet}]", f"[{alphabet}]", f"[{alphabet}]", f"[{alphabet}]"]
    while True:

        word = ""
        matches = re.findall(r"".join(MATCH), FILE)
        for match in matches:
            if match not in wrongs:
                word = match
                break

        # Send Guess
        proc.sendline(word)

        review = proc.recvline().decode().replace(GREEN, "G").replace(YELLOW, "Y").replace(WHITE, "W")[-10:-1].split(
            " ")

        for i, char in enumerate(review, 0):
            if char == 'W':
                alphabet = alphabet.replace(word[i], "")
            elif char == 'Y':
                MATCH[i] = MATCH[i].replace(word[i], "")
            else:
                MATCH[i] = f"[{word[i]}]"

        if "W" in review or "Y" in review:
            wrongs.append(word)
        else:

            print(f"Correct with {word} in {WORDS.index(word)}")

            # Calculate id
            hexId = int(hexId, 16)
            moded = WORDS.index(word)
            id = (hexId ^ moded) * len(WORDS) + moded

            print(f"Calculated Id {id}")

            return id


def bingo(proc: pwn.remote, crack: int):
    print("=== Bingo Start ===")
    # print(f"Receive generated id {proc.recvline().decode().strip()}")
    line = proc.recvline_regex(r"Round (\d{1,3}): #([\w\d]+)\n")
    round, hexId = re.findall(r"Round (\d{1,3}): #([\w\d]+)", line.decode())[0]
    round = int(round)

    answer = WORDS[crack % len(WORDS)]
    id = (crack // len(WORDS)) ^ (crack % len(WORDS))
    print(f"round {round}, hexId is {hex(id)[2:].zfill(5)}, answer is {answer}, crack Id is {crack}")
    assert hex(id)[2:].zfill(5) == hexId

    proc.sendline(answer)
    print(proc.recvline().decode())
# wordle
from randcrack import RandCrack
from pwn import *
from wordleLib import guess, bingo

MODE = 0
RC = RandCrack()

proc = remote("192.168.5.129", 1234)

proc.recvuntil(b'> ')
proc.sendline(str(MODE).encode())
for x in range(512):
    if x > 350:
        print(f"Predict next number as {RC.predict_randrange(4090 * (2 ** 20))}")
        RC.submit(guess(proc))
    else:
        guess(proc)

proc.recvuntil(b'> ')
proc.sendline(str(MODE).encode())
for x in range(512):
    print(f"Predict next number as {RC.predict_randrange(4090 * (2 ** 20))}")
    RC.submit(guess(proc))

MODE = 3
proc.recvuntil(b'> ')
proc.sendline(str(MODE).encode())
for x in range(512):
    bingo(proc, RC.predict_randrange(4090 * (2 ** 20)))
proc.interactive()

完成两轮模式 0 的游戏之后即可将模式改为 3 并预测游戏结果。进而在游戏限制下通过模式 3。

Ranma½

将附件使用如下 CyberChef Receipt 处理后可得相对可读的内容。

Decode_text('UTF-8 (65001)')

也可以使用 vim 的 set fileencoding 将编码转为 ANSI。

KGR/QRI 10646-1 zswtqgg d tnxcs tsdtofbrx osk ndnzhl gna Ietygfviy Idoilfvsu Arz (QQJ) hkkqk maikaglvusv ubyp cw ekg krzyj'o kitwkbj alypsdd.  Wjs rzvmebrwoa duwcuosu pqecgqamo cw ekg IFA, uussmpu, ysum aup qfxschljyk swks pcbb khxnsee drdoqpgpwfyv cbg xeupctzou, oql gneg ylv nsg bb zds upygzrxzkjh fq XVT-8, wpr uxxvnw qt wpvy isdz. XVT-8 kif zds tsdtofbrxegktf qt szryafmtqi hkm sahz LD-DUQLQ egjuv, auqjllvtc qfxschljvrehp hlvv iqyk omjehog, sieyafj lqf cwprx ocwezcfh bugp fvwb qb XA-NYYWZ gdniha oap oip wtoqacgnsee wq cwprx rocfhu. HTTPZB{QFOLP6_KRZ1Q}

猜测是 Vigenère 加密,因此直接爆破密钥可得 codingworld。解密内容后可得如下信息,flag 位于其末尾。

ISO/IEC 10646-1 defines a large character set called the Universal Character Set (UCS) which encompasses most of the world's writing systems.  The originally proposed encodings of the UCS, however, were not compatible with many current applications and protocols, and this has led to the development of UTF-8, the object of this memo. UTF-8 has the characteristic of preserving the full US-ASCII range, providing compatibility with file systems, parsers and other software that rely on US-ASCII values but are transparent to other values. TQLCTF{CODIN6_WOR1D}
TQLCTF{CODIN6_WOR1D}

UTF-8 可变长编码

附件采用的编码其实是 UTF-8,但是经过了特殊的变长编码。

因此可以使用脚本来一把梭解码并完成这道题。

from libcodebusters import Decrypt
from morse3 import Morse

data = open("flag_4c7b25b7ade73ac3a6b3081c81633fe6", "rb").read()
text = morse = key = ""

for x in range(len(data)):
    if data[x] >= 0xE0:
        text += chr(((data[x] ^ 0xE0) << 12) + ((data[x + 1] ^ 0x80) << 6) + (data[x + 2] ^ 0x80))
        morse += " "

    elif data[x] >= 0xC0:
        text += chr(((data[x] ^ 0xC0) << 6) + (data[x + 1] ^ 0x80))
        morse += "-"
    elif data[x] >= 0x80:
        continue
    else:
        text += chr(data[x])
        morse += "."

morse = morse[:43]
key = Morse(morse).morseToString()
print(Decrypt.vigenere(text, key))

the Ohio State University

将谱面文件解压后筛选出修改时间更新的文件,发现有两个难度的谱面被修改了。在官网下载原版的谱面可以进行比较。

谱面二进制

VIVID 难度的谱面的末尾被改成了绝赞纵连,而且重复度很高。

考虑是两个拍子合起来作为一个八位二进制存储信息。因此写出下列脚本将内容提取出来。

import re

data = open("./Modified/MisoilePunch - VVelcome!! (Fresh Chicken) [VIVID].osu", "rb").read().decode()
data = re.findall(r"(\d{2,3}),192,(\d{6}),1,0,0:0:0:0:", data)
now = 0
byte = []
chars = 0
for position, time in data:
    if int(time) > now:
        if now != 0:
            byte.append(f"{chars:04d}")
        chars = 0
        now = int(time)
    if position == "64":
        chars += 1000
    elif position == "192":
        chars += 100
    elif position == "320":
        chars += 10
    elif position == "448":
        chars += 1
    else:
        pass
byte = "".join(byte)
byte = [chr(int(byte[x : x + 8], 2)) for x in range(0, len(byte), 8)]
byte = "".join(byte)
print(byte[66 : -5 : 4])

得到的内容如下。

5HoWtIme}

SilentEye wave 隐写

在 BASIC 的谱面中可以发现如下信息。

WAVPassword: MisoilePunch

结合文件中被修改的音频,使用 SilentEye 尝试进行解码。

可以得到如下信息。

_TO_O$u_i7s_

Steghide 隐写

封面图的属性中可以发现一段信息。

pwd: VVelcome!!

使用上述的密码对图片施加 steghide 可得如下内容。

TQLCTF{VVElcOM3

将上述得到的信息拼接即可得到 flag。

TQLCTF{VVElcOM3_TO_O$u_i7s_5HoWtIme}

osu! 好难,我还在打三星的谱子,不会打 Mania。

results matching ""

    No results matching ""